/* eslint-disable @typescript-eslint/no-explicit-any */
import { cls } from '@knno/jsx';
import { contentEditable } from '../../contentEditable';
import { ALLOW_CONTROLS, selectAll, setRangeByPos, setSelection } from '../../editor';
import { cellStyle } from '../cell.css';
import { inlineStyle } from '../inline.css';
import { InputBase, type InputBaseProps } from '../inputBase';
import { textInputStyle } from './text.css';

export type TextInputProps<T extends TextInputBase<any, any, any, any>, V> = InputBaseProps<
	T,
	V
> & {
	multiline?: boolean;
	maxLength?: number;
	/**
	 * MinLength and pattern are only used for validation.
	 * They are not used to limit the input.
	 */
	minLength?: number;
	pattern?: RegExp;
};

export abstract class TextInputBase<
	P extends TextInputProps<C, V>,
	C extends TextInputBase<P, C, E, V>,
	E extends HTMLElement,
	V = string,
> extends InputBase<P, C, E, V> {
	constructor(props: P) {
		super(props);
		this.el.classList.add(textInputStyle);
		this.el.tabIndex = 0;
		let ime = false;
		this.el.addEventListener('compositionstart', () => {
			ime = true;
		});
		this.el.addEventListener('compositionend', () => {
			ime = false;
			this.updateValue((this.el.textContent as V) || undefined);
		});
		this.el.addEventListener('input', () => {
			if (!ime) {
				this.updateValue((this.el.textContent as V) || undefined);
			}
		});
		this.el.addEventListener('keydown', (evt) => {
			if (evt.key === 'Enter' && this.props.multiline !== true) {
				evt.preventDefault();
			} else {
				const sel = window.getSelection();
				if (!sel || sel.rangeCount === 0) return;
				const range = sel.getRangeAt(0);
				if (
					!evt.ctrlKey &&
					range.collapsed &&
					!ALLOW_CONTROLS.has(evt.key) &&
					this.maxLength !== undefined &&
					this.el.textContent?.length !== undefined &&
					this.el.textContent.length >= this.maxLength
				) {
					evt.preventDefault();
				}
			}
		});
		let middleButtonDown = false;
		let mouseDown = false;
		this.el.addEventListener('mouseup', (evt) => {
			mouseDown = false;
			if (evt.button === 1) {
				setTimeout(() => {
					middleButtonDown = false;
				});
				evt.preventDefault();
			}
		});
		this.el.addEventListener('mousedown', (evt) => {
			mouseDown = true;
			if (evt.buttons === 4) {
				middleButtonDown = true;
				this.el.focus();
				evt.preventDefault();
				selectAll(this.el);
			}
		});
		this.el.addEventListener('dragover', (evt) => {
			// evt.preventDefault();
			// if call preventDefault caret will disappear
			evt.stopPropagation();
			setRangeByPos(evt.clientX, evt.clientY);
		});
		this.el.addEventListener('drop', (evt) => {
			evt.preventDefault();
			evt.stopPropagation();
			const clipboardData = evt.dataTransfer;
			if (!clipboardData) return;
			this.paste(clipboardData);
		});
		this.el.addEventListener('paste', (evt) => {
			evt.preventDefault();
			if (middleButtonDown) return;
			const clipboardData = evt.clipboardData;
			if (!clipboardData) return;
			this.paste(clipboardData);
		});
		this.el.addEventListener('focus', () => {
			if (this.readOnly) return;
			if (!this.noSelectAll) {
				setTimeout(() => {
					if (!mouseDown) {
						const range = document.createRange();
						if (this.el.textContent && this.el.firstChild) {
							// range.selectNodeContents(this.el.firstChild);
							range.setStart(this.el.firstChild, 0);
							range.setEnd(this.el.lastChild!, this.el.lastChild!.textContent!.length);
							setSelection(range);
						}
					}
				});
			}
		});
	}

	protected get noSelectAll(): boolean {
		return false;
	}

	private format() {
		if (
			this.maxLength !== undefined &&
			this.el.textContent?.length !== undefined &&
			this.el.textContent.length >= this.maxLength
		) {
			this.el.textContent = this.el.textContent.slice(0, this.maxLength);
			this.updateValue((this.el.textContent as V) || undefined);
		}
	}

	protected override renderContent(): void {
		this.el.textContent = this.value ? this.value + '' : '';
		this.format();
	}

	protected override changeReadOnly(): void {
		if (!this.props.readOnly) {
			this.el.setAttribute('contenteditable', contentEditable);
		} else {
			this.el.removeAttribute('contenteditable');
		}
	}

	private paste(clipboardData: DataTransfer) {
		if (this.readOnly) return;
		if (clipboardData.types.includes('text/plain')) {
			const sel = window.getSelection();
			if (!sel || sel.rangeCount === 0) return;
			const range = sel.getRangeAt(0);
			range.deleteContents();
			range.collapse();
			let txt = clipboardData.getData('text/plain');
			if (!this.props.multiline && /[\r\n]/.test(txt)) {
				txt = txt.replace(/[\r\n]/g, '');
			}
			range.insertNode(document.createTextNode(txt));
			range.collapse();
			if (
				this.maxLength !== undefined &&
				this.el.textContent?.length !== undefined &&
				this.el.textContent.length >= this.maxLength
			) {
				this.el.textContent = this.el.textContent.slice(0, this.maxLength);
				if (this.el.firstChild) {
					range.setStart(this.el.firstChild, this.maxLength);
					range.collapse();
				}
			}
			this.updateValue((this.el.textContent as V) || undefined);
		}
	}

	get multiline(): boolean {
		return this.props.multiline ?? false;
	}
	setMultiline(multiline: boolean): this {
		this.props.multiline = multiline;
		if (!multiline && typeof this.value === 'string') {
			this.setValue(this.value.replaceAll(/[\r\n]/g, '') as V);
		}
		return this;
	}
	get maxLength(): number | undefined {
		return this.props.maxLength;
	}
	setMaxLength(value: number | undefined): this {
		this.props.maxLength = value;
		this.format();
		return this;
	}
	get minLength(): number | undefined {
		return this.props.minLength;
	}
	setMinLength(value: number | undefined): this {
		this.props.minLength = value;
		return this;
	}
}

export type TextCellProps = TextInputProps<TextCell, string> & {
	colSpan?: number;
	rowSpan?: number;
};

export class TextCell extends TextInputBase<TextCellProps, TextCell, HTMLTableCellElement> {
	protected render(): Node {
		return (
			<td
				class={cls(this.props.class, cellStyle)}
				style={this.props.style}
				colSpan={this.colSpan}
				rowSpan={this.rowSpan}
			/>
		);
	}
	get colSpan(): number {
		return this.props.colSpan ?? 1;
	}

	setColSpan(value: number): this {
		this.props.colSpan = value;
		this.el.colSpan = value;
		return this;
	}

	get rowSpan(): number {
		return this.props.rowSpan ?? 1;
	}

	setRowSpan(value: number): this {
		this.props.rowSpan = value;
		this.el.rowSpan = value;
		return this;
	}
}

export class TextInline extends TextInputBase<
	TextInputProps<TextInline, string>,
	TextInline,
	HTMLSpanElement
> {
	protected render(): Node {
		return <span class={cls(this.props.class, inlineStyle)} style={this.props.style} />;
	}
}
